/*
 * 代号：凤凰
 * http://www.jphenix.org
 * 2014-06-13
 * V4.0
 */
package com.jphenix.webserver.servlet;

import com.jphenix.share.tools.MD5;
import com.jphenix.standard.docs.ClassInfo;
import com.jphenix.webserver.instancea.Identification;
import com.jphenix.webserver.instancea.ThrottleItem;
import com.jphenix.webserver.instancea.ThrottledOutputStream;
import com.jphenix.webserver.instancea.WildcardDictionary;
import com.jphenix.webserver.interfaceclass.IGlobalVar;
import com.jphenix.webserver.interfaceclass.IServeParameter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * 文件Servlet
 * 
 * 2018-10-30 在找不到文件时，在日志中输出详细原因
 * 2018-11-02 修改了设置了虚拟路径时，启用内部服务器，虚拟路径设置错误的问题
 * 2019-08-14 在报文头中增加了ETag标签
 * 2019-09-17 整理了代码格式
 * 
 * @author 刘虻
 * 2006-9-15上午12:10:31
 */
@ClassInfo({"2019-09-17 10:43","文件处理类"})
public class FileServlet extends ASuperServlet {

	/**
	 * 版本标识
	 */
	private static final long             serialVersionUID  = 3291890302071912096L;
	private static final int              COPY_BUF_SIZE     = 4096 * 2; //处理流缓存大小
	private static final SimpleDateFormat shortfmt          = new SimpleDateFormat("MMM dd HH:mm");
	private static final SimpleDateFormat longfmt           = new SimpleDateFormat("MMM dd yyyy");

	private static WildcardDictionary     throttleTab       = null;
	private static String[]               DEFAULTINDEXPAGES = {"index.html","index.htm", "default.htm", "default.html" };
	private static final DecimalFormat    lengthftm         = new DecimalFormat("#");
	private String                        charSet           = IGlobalVar.ISO88591;// "iso-8859-1";
	private static final boolean          logenabled        = false;
	private IServeParameter               serveParameter    = null; // 配置文件处理类
	private int                           noListFile        = 0;    //是否不显示文件列表  1显示 2不显示 0未初始化
	
	/**
	 * 构造函数 2008-6-28下午04:40:09
	 */
	public FileServlet() {
		super();
	}

	/**
	 * 构造函数
	 * 
	 * @param throttles
	 *            配置文件路径
	 * @param charset
	 *            读取文件编码(不是读取配置文件) 2008-6-28下午04:40:48
	 */
	public FileServlet(String throttles, String charset) throws IOException {
		this();
		if (charset != null && charset.length() > 0) {
			this.charSet = charset;
		}
		readThrottles(throttles);
	}

	/**
	 * 构造函数 2009-7-22下午03:22:00
	 */
	public FileServlet(String throttles, IServeParameter serveParameter)
			throws IOException {
		this();
		this.serveParameter = serveParameter;
		if (serveParameter != null) {
			String charSet = serveParameter
					.getArgumentString(IGlobalVar.ARG_SERVLET_TRANSFER_ENCODING);
			if (charSet.length() > 0) {
				this.charSet = charSet;
			}
		}
		if(serveParameter.getWelcomeFileList().size()>0) {
			//重新放入默认页面序列
			DEFAULTINDEXPAGES = new String[serveParameter.getWelcomeFileList().size()];
			serveParameter.getWelcomeFileList().toArray(DEFAULTINDEXPAGES);
		}
		readThrottles(throttles);
	}

	/**
	 * 返回新的类实例
	 * 
	 * @author 刘虻 2008-6-28下午04:37:59
	 * @return 新的类实例
	 */
	public static HttpServlet newInstance() {
		return new FileServlet();
	}

	/**
	 * 返回新的类实例
	 * 
	 * @author 刘虻 2008-6-28下午04:37:59
	 * @param throttles
	 *            配置信息文件路径
	 * @param charset
	 *            编码
	 * @return 新的类实例
	 */
	public static HttpServlet newInstance(String throttles,
			IServeParameter serveParameter) throws IOException {
		return new FileServlet(throttles, serveParameter);
	}

	/**
	 * 读取配置信息
	 * 
	 * @author 刘虻 2008-6-28下午04:42:55
	 * @param throttles
	 *            配置文件路径
	 * @throws IOException
	 *             读取配置信息发生异常
	 */
	protected void readThrottles(String throttles) throws IOException {
		WildcardDictionary newThrottleTab = ThrottledOutputStream
				.parseThrottleFile(throttles);
		if (throttleTab == null) {
            throttleTab = newThrottleTab;
        } else {
			// Merge the new one into the old one.
			Enumeration<String> keys = newThrottleTab.keys();
			Enumeration<Object> elements = newThrottleTab.elements();
			while (keys.hasMoreElements()) {
				String key = keys.nextElement();
				Object element = elements.nextElement();
				throttleTab.put(key, element);
			}
		}
	}

	/**
	 * 覆盖方法
	 * 
	 * @author 刘虻 2008-7-9上午08:18:20
	 */
	@Override
    public String getServletInfo() {
		return "servlet similar to a standard httpd";
	}

	/**
	 * 覆盖方法
	 * 
	 * @author 刘虻 2008-7-9上午08:18:29
	 */
	@Override
    public void service(HttpServletRequest req, HttpServletResponse res)
			throws ServletException, IOException {
		boolean headOnly;
		headOnly = "head".equalsIgnoreCase(req.getMethod());
		
		/*
		 * 修改 开始
		 * 发现在用AJAX调用后，马上做window.location时，报文头开始并不是GET
		 * 而是一堆上一次提交的参数，导致Method无法辨认
		 */
//		if (req.getMethod().equalsIgnoreCase("get")
//				|| req.getMethod().equalsIgnoreCase("post"))
//			headOnly = false;
//		else if (req.getMethod().equalsIgnoreCase("head"))
//			headOnly = true;
//		else {
//			res.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
//			return;
//		}
		/*修改结束*/
		
		// req.setCharacterEncoding(IGlobalVar.UTF8);
		String path = canonicalizePath(req.getPathInfo());

		dispatchPathName(req, res, headOnly, path);
	}


	/**
	 * 解析目标文件
	 * 刘虻
	 * 2012-7-27 上午9:04:46
	 * @param req							页面请求
	 * @param res							页面反馈
	 * @param headOnly				只有报文头
	 * @param path						文件路径
	 * @throws IOException			异常
	 */
	protected void dispatchPathName(HttpServletRequest req,
			HttpServletResponse res, boolean headOnly, String path)
			throws IOException {
		String filename = req.getPathTranslated() != null ? req
				.getPathTranslated().replace('/', File.separatorChar) : "";
		File file = new File(filename);
		log("showing '" + filename + "' for path " + path);
		if (file.exists()) {
			if (!file.isDirectory()) {
				serveFile(req, res, headOnly, path, file);
			} else {
				if(isListFile()) {
					log("showing dir " + file);
					if (redirectDirectory(req, res, path, file) == false) {
						showIdexFile(req, res, headOnly, path, filename);
					}
				}else {
					res.sendError(HttpServletResponse.SC_NOT_FOUND);
				}
			}
		} else {
			System.out.println("The URL Not Found ["+req.getRequestURL()+"] referer:["+req.getHeader("Referer")+"] FilePath:["+filename+"]");
			res.sendError(HttpServletResponse.SC_NOT_FOUND,"The URL Not Found [<b>"+req.getRequestURL()+"</b>]");
		}
	}

	/**
	 * 母鸡
	 * 
	 * @author 刘虻 2008-7-9上午08:19:06
	 * @param req
	 * @param res
	 * @param headOnly
	 * @param path
	 * @param parent
	 * @throws IOException
	 */
	protected void showIdexFile(HttpServletRequest req,
			HttpServletResponse res, boolean headOnly, String path,
			String parent) throws IOException {
		
		log("showing index in directory " + parent);
		for (int i = 0; i < DEFAULTINDEXPAGES.length; i++) {
			File indexFile = new File(parent, DEFAULTINDEXPAGES[i]);
			if (indexFile.exists()) {
				serveFile(req, res, headOnly, path, indexFile);
				return;
			}
		}
		// index not found
		serveDirectory(req, res, headOnly, path, new File(parent));
	}


	/**
	 * 输出指定文件内容到页面中
	 * 刘虻
	 * 2012-7-27 上午9:09:36
	 * @param req						页面请求
	 * @param res						页面反馈
	 * @param headOnly			是否只有报文头
	 * @param path					文件路径
	 * @param file						文件对象
	 * @throws IOException		异常
	 */
	protected void serveFile(HttpServletRequest req, HttpServletResponse res,
			boolean headOnly, String path, File file) throws IOException {
		log("getting " + file);
		if (!file.canRead()) {
			res.sendError(HttpServletResponse.SC_FORBIDDEN);
			return;
		} else
			// by Niel Markwick
        {
            try {
                file.getCanonicalPath();
            } catch (Exception e) {
                res.sendError(HttpServletResponse.SC_FORBIDDEN,
                        "Forbidden, exception:" + e);
                return;
            }
        }

		// Handle If-Modified-Since.
		res.setStatus(HttpServletResponse.SC_OK);
		long lastMod = file.lastModified();
		String ifModSinceStr = req.getHeader("If-Modified-Since");
		long ifModSince = -1;
		if (ifModSinceStr != null) {
			int semi = ifModSinceStr.indexOf(';');
			if (semi != -1) {
                ifModSinceStr = ifModSinceStr.substring(0, semi);
            }
			try {
				ifModSince = DateFormat.getDateInstance().parse(ifModSinceStr)
						.getTime();
			} catch (Exception ignore) {
				log("Can't parse " + ifModSinceStr + " " + ignore);
			}
		}
		if (ifModSince != -1 && ifModSince >= lastMod) {
			res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
			headOnly = true;
		}

		res.setContentType(getServletContext().getMimeType(file.getName()));
		if (file.length() < Integer.MAX_VALUE) {
			res.setContentLength((int) file.length());
		}else {
			res.setHeader("Content-Length", Long.toString(file.length()));
		}
		
		res.setDateHeader("Last-modified", lastMod);
		res.setHeader("ETag",MD5.md5(file.getPath()));

		OutputStream out = null;
		InputStream in = null;
		try {
			out = res.getOutputStream();
			if (!headOnly) {
				// Check throttle.
				if (throttleTab != null) {
					ThrottleItem throttleItem = (ThrottleItem) throttleTab
							.get(path);
					if (throttleItem != null) {
						// !!! Need to account for multiple simultaneous
						// fetches.
						out = new ThrottledOutputStream(out, throttleItem
								.getMaxBps());
					}
				}

				in = new FileInputStream(file);
				copyStream(in, out);
			}
		} finally {
			if (in != null) {
                try {
                    in.close();
                } catch (IOException ioe) {
                }
            }
			if (out != null) {
				out.flush();
				out.close();
			}
		}
	}


	/**
	 * 处理流
	 * 刘虻
	 * 2012-7-27 上午9:14:27
	 * @param in						读入流
	 * @param out					输出流
	 * @throws IOException		异常
	 */
	public void copyStream(InputStream in, OutputStream out) throws IOException {
		byte[] buf = new byte[COPY_BUF_SIZE];
		int len;
		while ((len = in.read(buf)) != -1) {
			out.write(buf, 0, len);
		}
	}

	/**
	 * 母鸡
	 * 
	 * @author 刘虻 2008-7-9上午08:19:30
	 * @param req
	 * @param res
	 * @param headOnly
	 * @param path
	 * @param file
	 * @throws IOException
	 */
	protected void serveDirectory(HttpServletRequest req,
			HttpServletResponse res, boolean headOnly, String path, File file)
			throws IOException {
		log("indexing " + file);
		if (!file.canRead()) {
			res.sendError(HttpServletResponse.SC_FORBIDDEN);
			return;
		}
		if(!isListFile()) {
			res.sendError(HttpServletResponse.SC_NOT_FOUND);
			return;
		}
		if(path.toLowerCase().startsWith("web-inf")) {
			res.sendError(HttpServletResponse.SC_NOT_FOUND);
			return;
		}
		res.setStatus(HttpServletResponse.SC_OK);
		res.setContentType("text/html;charset=" + charSet);
		OutputStream out = res.getOutputStream();
		if (!headOnly) {
			PrintStream p = new PrintStream(new BufferedOutputStream(out),
					false, charSet); // 1.4
			p.println("<HTML><HEAD>");
			p
					.println("<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset="
							+ charSet + "\">");
			p.println("<TITLE>Index of " + path + "</TITLE>");
			p.println("</HEAD><BODY " + IGlobalVar.BGCOLOR);
			p.println("><H2>Index of " + path + "</H2>");
			p.println("<PRE>");
			p.println("mode         bytes  last-changed    name");
			p.println("<HR>");
			String[] names = file.list();
			Arrays.sort(names);
			long total = 0;
			for (int i = 0; i < names.length; ++i) {
				if("web-inf".equals(names[i].toLowerCase())) {
					continue;
				}
				File aFile = new File(file, names[i]);
				String aFileType;
				long aFileLen;
				if (aFile.isDirectory()) {
                    aFileType = "d";
                } else if (aFile.isFile()) {
                    aFileType = "-";
                } else {
                    aFileType = "?";
                }
				String aFileRead = (aFile.canRead() ? "r" : "-");
				String aFileWrite = (aFile.canWrite() ? "w" : "-");
				String aFileExe = "-";
				String aFileSize = lengthftm.format(aFileLen = aFile.length());
				total += (aFileLen + 1023) / 1024; // 
				while (aFileSize.length() < 12) {
                    aFileSize = " " + aFileSize;
                }
				String aFileDate = lsDateStr(new Date(aFile.lastModified()));
				while (aFileDate.length() < 14) {
                    aFileDate += " ";
                }
				String aFileDirsuf = (aFile.isDirectory() ? "/" : "");
				String aFileSuf = (aFile.isDirectory() ? "/" : "");
				p.println(aFileType + aFileRead + aFileWrite + aFileExe + "  "
						+ aFileSize + "  " + aFileDate + "  " + "<A HREF=\""
						+ URLEncoder.encode(names[i], charSet) /* 1.4 */
						+ aFileDirsuf + "\">" + names[i] + aFileSuf + "</A>");
			}
			if (total != 0) {
                total += 3;
            }
			p.println("total " + total);
			p.println("</PRE>");
			p.println("<HR>");
			Identification.writeAddress(p);
			p.println("</BODY></HTML>");
			p.flush();
		}
		out.close();
	}

	/**
	 * 母鸡
	 * 
	 * @author 刘虻 2008-7-9上午07:42:31
	 * @param date
	 *            母鸡
	 * @return 母鸡
	 */
	protected String lsDateStr(Date date) {
		if (Math.abs(System.currentTimeMillis() - date.getTime()) < 183L * 24L
				* 60L * 60L * 1000L) {
			return shortfmt.format(date);
		} else {
			return longfmt.format(date);
		}
	}

	/**
	 * 母鸡
	 * 
	 * @author 刘虻 2008-7-9上午08:19:54
	 * @param req
	 * @param res
	 * @param path
	 * @param file
	 * @return
	 * @throws IOException
	 */
	protected boolean redirectDirectory(HttpServletRequest req,
			HttpServletResponse res, String path, File file) throws IOException {
		int pl = path.length();
		if (pl > 0 && path.charAt(pl - 1) != '/') {
			// relative redirect
			int sp = path.lastIndexOf('/');
			if (sp < 0) {
                path += '/';
            } else {
                path = path.substring(sp + 1) + '/';
            }
			log("redirecting dir " + path);
			res.sendRedirect(path);
			return true;
		}
		return false;
	}

	/**
	 * 覆盖方法
	 * 
	 * @author 刘虻 2008-7-9上午08:19:59
	 */
	@Override
    public void log(String msg) {
		if (logenabled) {
            super.log(msg);
        }
	}


	/**
	 * 处理目标文件相对路径
	 * 刘虻
	 * 2012-7-27 上午9:01:40
	 * @param path 页面请求中的文件路径信息
	 * @return 处理后的文件相对路径
	 */
	protected String canonicalizePath(String path) {
		if (path == null || path.length() == 0) {
            return path;
        }
		List<String> pathElems = new ArrayList<String>(6);
		char[] pa = path.toCharArray();
		int n = pa.length;
		int s = -1;
		int lev = 0;
		for (int i = 0; i < n; i++) {
			if (s < 0) {
				if (pa[i] != '/' && pa[i] != '\\') {
                    s = i;
                }
			} else {
				boolean f = false;
				if (pa[i] == '?') {
                    f = true;
                }
				if (pa[i] == '/' || pa[i] == '\\' || f) {
					String el = new String(pa, s, i - s);
					if ("..".equals(el)) {
						if (pathElems.size() > 0) {
                            pathElems.remove(pathElems.size() - 1);
                        } else {
                            lev--;
                        }
						// else exception ?
					} else if (".".equals(el) == false) {
                        if (lev >= 0) {
                            pathElems.add(el);
                        } else {
                            lev++;
                        }
                    }
					if (f) {
						s = i;
						break;
					}
					s = -1;
				}
			}
		}
		if (s > 0) {
			String el = new String(pa, s, n - s);
			if ("..".equals(el)) {
				if (pathElems.size() > 0) {
                    pathElems.remove(pathElems.size() - 1);
                }
				// else exception ?
			} else if (".".equals(el) == false) {
                if (lev >= 0) {
                    pathElems.add(el);
                }
            }
		} else {
            pathElems.add("");
        }
		if (pathElems.size() == 0) {
            return "";
        }
		StringBuffer result = new StringBuffer(n);
		result.append(pathElems.get(0));
		n = pathElems.size();
		for (int i = 1; i < n; i++) {
            result.append('/').append(pathElems.get(i));
        }
		// System.err.println("Before "+path+" after "+result);
		return result.toString();
	}

	/**
	 * 是否不显示文件列表
	 * 默认不显示列表
	 * @author 刘虻
	 * 2009-9-22下午02:17:54
	 * @return 是否不显示文件列表
	 */
	protected boolean isListFile() {
		//noListFile: 是否不显示文件列表  1显示 2不显示 0未初始化
		if(noListFile==0) {
			if(serveParameter.getArgumentBoolean(IGlobalVar.ARG_LIST_FILE)) {
				noListFile = 1;
			}else {
				noListFile = 2;
			}
		}
		return noListFile == 1;
	}
}